/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.firebase.dataconnect

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.modules.SerializersModule

/**
 * Information about a Firebase Data Connect "operation" (a query or a mutation).
 *
 * [OperationRef] has two inheritors: [QueryRef] for queries and [MutationRef] for mutations.
 * [OperationRef] merely serves to provide a common interface for the parts of queries and mutations
 * that are shared.
 *
 * ### Safe for concurrent use
 *
 * All methods and properties of [OperationRef] are thread-safe and may be safely called and/or
 * accessed concurrently from multiple threads and/or coroutines.
 *
 * ### Not stable for inheritance
 *
 * The [OperationRef] interface is _not_ stable for inheritance in third-party libraries, as new
 * methods might be added to this interface or contracts of the existing methods can be changed.
 */
public interface OperationRef<Data, Variables> {

  /** The [FirebaseDataConnect] with which this object is associated. */
  public val dataConnect: FirebaseDataConnect

  /**
   * The name of the operation, as defined in GraphQL.
   *
   * For example, a query defined as
   *
   * ```
   * query GetPersonById($id: UUID!) { person(id: $id) { name age } }
   * ```
   *
   * would have the operation name `"GetPersonById"` and a mutation defined as
   *
   * ```
   * mutation InsertPerson($name: String!, $age: Int) {...}
   * ```
   *
   * would have the operation name `"InsertPerson"`
   */
  public val operationName: String

  /**
   * The variables for the operation.
   *
   * The variables will be serialized using [variablesSerializer] and must produce a map whose keys
   * are strings whose values are the names of the variables as defined in GraphQL, and whose values
   * are the corresponding values.
   *
   * For example, a query defined as
   *
   * ```
   * query GetPersonById($id: UUID!) { person(id: $id) { name age } }
   * ```
   *
   * would have a variable named `"id"` whose value is a [java.util.UUID] instance and a mutation
   * defined as
   *
   * ```
   * mutation InsertPerson($name: String!, $age: Int) {...}
   * ```
   *
   * would have two variables named `"name"` and `"age"` whose values are [String] and `Int?`
   * values, respectively.
   */
  public val variables: Variables

  /**
   * The deserializer to use to deserialize the response data for this operation.
   *
   * Typically, the deserializer will be generated by Kotlin's serialization plugin for a class
   * annotated with [kotlinx.serialization.Serializable].
   *
   * For example, a query defined as
   *
   * ```
   * query GetPersonById($id: UUID!) { person(id: $id) { name age } }
   * ```
   *
   * could define its data class as follows:
   *
   * ```
   * @Serializable
   * data class GetPersonByIdData(val person: Person?) {
   *   @Serializable
   *   data class Person(val name: String, val age: Int?)
   * }
   * ```
   *
   * and the deserializer could be retrieved by calling [kotlinx.serialization.serializer] as
   * follows:
   *
   * ```
   * serializer<GetPersonByIdData>()
   * ```
   */
  public val dataDeserializer: DeserializationStrategy<Data>

  /**
   * The serializer to use to serialize the variables for this operation.
   *
   * Typically, the serializer will be generated by Kotlin's serialization plugin for a class
   * annotated with [kotlinx.serialization.Serializable].
   *
   * For example, a mutation defined as
   *
   * ```
   * mutation InsertPerson($name: String!, $age: Int) {...}
   * ```
   *
   * could define its variables class as follows:
   *
   * ```
   * @Serializable
   * data class InsertPersonVariables(val name: String, val age: Int?)
   * ```
   *
   * and the serializer could be retrieved by calling [kotlinx.serialization.serializer] as follows:
   *
   * ```
   * serializer<InsertPersonVariables>()
   * ```
   */
  public val variablesSerializer: SerializationStrategy<Variables>

  /**
   * The [FirebaseDataConnect.CallerSdkType] that will be associated with all operations performed
   * by this object for analytics purposes.
   */
  public val callerSdkType: FirebaseDataConnect.CallerSdkType

  /**
   * A [SerializersModule] to use when decoding the response data using [dataDeserializer]. May be
   * `null`, to not use a [SerializersModule].
   */
  public val dataSerializersModule: SerializersModule?

  /**
   * A [SerializersModule] to use when encoding the variables using [variablesSerializer]. May be
   * `null`, to not use a [SerializersModule].
   */
  public val variablesSerializersModule: SerializersModule?

  /**
   * Executes this operation and returns the result.
   *
   * An exception is thrown if the operation fails for any reason, including
   * - The [FirebaseDataConnect] object has been closed.
   * - The Firebase Data Connect server is unreachable.
   * - Authentication with the Firebase Data Connect server fails.
   * - The variables are rejected by the Firebase Data Connect server.
   * - The data response sent by the Firebase Data Connect server cannot be deserialized.
   */
  public suspend fun execute(): OperationResult<Data, Variables>

  /**
   * Creates and returns a new object that is a _copy_ of this object, but with the properties whose
   * names corresponding to the given arguments changed to the respective argument's value.
   *
   * This function is essentially the same as the `copy()` method that is generated by the Kotlin
   * compiler for `data class` classes.
   *
   * @see withDataDeserializer
   * @see withVariablesSerializer
   */
  @ExperimentalFirebaseDataConnect
  public fun copy(
    operationName: String = this.operationName,
    variables: Variables = this.variables,
    dataDeserializer: DeserializationStrategy<Data> = this.dataDeserializer,
    variablesSerializer: SerializationStrategy<Variables> = this.variablesSerializer,
    callerSdkType: FirebaseDataConnect.CallerSdkType = this.callerSdkType,
    dataSerializersModule: SerializersModule? = this.dataSerializersModule,
    variablesSerializersModule: SerializersModule? = this.variablesSerializersModule,
  ): OperationRef<Data, Variables>

  /**
   * Creates and returns a new object that is a _copy_ of this object, just like [copy], except that
   * the [variables] and [variablesSerializer] can be a different type than [Variables].
   *
   * @see copy
   * @see withDataDeserializer
   */
  @ExperimentalFirebaseDataConnect
  public fun <NewVariables> withVariablesSerializer(
    variables: NewVariables,
    variablesSerializer: SerializationStrategy<NewVariables>,
    variablesSerializersModule: SerializersModule? = this.variablesSerializersModule,
  ): OperationRef<Data, NewVariables>

  /**
   * Creates and returns a new object that is a _copy_ of this object, just like [copy], except that
   * the [dataDeserializer] can be a different type than [Data].
   *
   * @see copy
   * @see withVariablesSerializer
   */
  @ExperimentalFirebaseDataConnect
  public fun <NewData> withDataDeserializer(
    dataDeserializer: DeserializationStrategy<NewData>,
    dataSerializersModule: SerializersModule? = this.dataSerializersModule,
  ): OperationRef<NewData, Variables>

  /**
   * Compares this object with another object for equality.
   *
   * @param other The object to compare to this for equality.
   * @return true if, and only if, the other object is an instance of the same implementation of
   * [OperationRef] whose public properties compare equal using the `==` operator to the
   * corresponding properties of this object.
   */
  override fun equals(other: Any?): Boolean

  /**
   * Calculates and returns the hash code for this object.
   *
   * The hash code is _not_ guaranteed to be stable across application restarts.
   *
   * @return the hash code for this object, that incorporates the values of this object's public
   * properties.
   */
  override fun hashCode(): Int

  /**
   * Returns a string representation of this object, useful for debugging.
   *
   * The string representation is _not_ guaranteed to be stable and may change without notice at any
   * time. Therefore, the only recommended usage of the returned string is debugging and/or logging.
   * Namely, parsing the returned string or storing the returned string in non-volatile storage
   * should generally be avoided in order to be robust in case that the string representation
   * changes.
   *
   * @return a string representation of this object, which includes the class name and the values of
   * all public properties.
   */
  override fun toString(): String
}

/**
 * The result of a successful execution of an [OperationRef].
 *
 * Typically, one of the inheritors of [OperationResult] is used, namely [QueryResult] for queries
 * and [MutationResult] for mutations.
 *
 * ### Safe for concurrent use
 *
 * All methods and properties of [OperationResult] are thread-safe and may be safely called and/or
 * accessed concurrently from multiple threads and/or coroutines.
 *
 * ### Not stable for inheritance
 *
 * The [OperationResult] interface is _not_ stable for inheritance in third-party libraries, as new
 * methods might be added to this interface or contracts of the existing methods can be changed.
 *
 * @see OperationRef.execute
 */
public interface OperationResult<Data, Variables> {
  /** The operation that produced this result. */
  public val ref: OperationRef<Data, Variables>

  /** The response data for the operation. */
  public val data: Data

  /**
   * Compares this object with another object for equality.
   *
   * @param other The object to compare to this for equality.
   * @return true if, and only if, the other object is an instance of the same implementation of
   * [OperationResult] whose public properties compare equal using the `==` operator to the
   * corresponding properties of this object.
   */
  override fun equals(other: Any?): Boolean

  /**
   * Calculates and returns the hash code for this object.
   *
   * The hash code is _not_ guaranteed to be stable across application restarts.
   *
   * @return the hash code for this object, that incorporates the values of this object's public
   * properties.
   */
  override fun hashCode(): Int

  /**
   * Returns a string representation of this object, useful for debugging.
   *
   * The string representation is _not_ guaranteed to be stable and may change without notice at any
   * time. Therefore, the only recommended usage of the returned string is debugging and/or logging.
   * Namely, parsing the returned string or storing the returned string in non-volatile storage
   * should generally be avoided in order to be robust in case that the string representation
   * changes.
   *
   * @return a string representation of this object, which includes the class name and the values of
   * all public properties.
   */
  override fun toString(): String
}
